package com.rikai.Rikai;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import java.util.zip.GZIPInputStream;

import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Environment;
import android.text.Html;
import android.text.SpannableString;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebView.HitTestResult;
import android.webkit.WebViewClient;
import android.widget.AbsoluteLayout;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.rikai.Rikai.Data.CompressedReader;

public class RikaiWebView extends LinearLayout {
  // public data
  public WebView webView;
  public Data data;
  public CompressedReader edict;
  public CompressedReader kanjidic;
  public Button kanjiButton;
  public Button dictButton;
  public Button currentButton;
  public static String lastMatch;
  public class VConjData {
    public byte[] derivedSuffix;
    public byte[] coreSuffix;
    public String tag;
  }
  public Vector<VConjData> vconj;
  public ProgressBar progress;
  public ParsedFile bookmarks;
  public String homepage;
  public ImageView finger;
  public Context context;
  private File bookmarksDir;
  private SharedPreferences sp;
  public Map<String, String> urlToTitle;

  // Constructors
  public RikaiWebView(Context c) {
    super(c);
    setup(c);
  }
  public RikaiWebView(Context c, AttributeSet as) {
    super(c,as);
    setup(c);
  }
  // Public methods
  public WebSettings getSettings() { return webView.getSettings(); }
  public void loadUrl(String s) { webView.loadUrl(s); } // mirroring silly webview spelling
  public boolean canGoBack() { return webView.canGoBack(); }
  public void goBack() { webView.goBack(); }
  public void setLastURL(String url) {
    if (url != null) try {
      SharedPreferences.Editor editor = sp.edit();
      editor.putString("lastURL", new URL(url).toExternalForm());
      editor.commit();
    } catch (Throwable t) {
      Log.e("RIKAI",t.toString());
    }
    //Log.e("RIKAI","Remember url " + url);
  }
  public String getLastURL(String def) { // generally def should == null or homepage
    return sp.getString("lastURL",def);
  }

  // Listener for button presses (and multi-touch)
  private class TouchieTouchie implements View.OnTouchListener {
    private RikaiWebView parent;
    Button mybut;
    float scale;
    long lastCall;
    float lastCallX;
    float lastCallY;
    
    public TouchieTouchie(RikaiWebView parent_, Button b) {
      parent = parent_;
      mybut = b;
      scale = parent.getContext().getResources().getDisplayMetrics().density;
    }
    public boolean onTouch(View v, MotionEvent ev) {
      int type = ev.getAction();
      if (type == MotionEvent.ACTION_DOWN) {
        return false;
      } else if (type == MotionEvent.ACTION_UP || type == MotionEvent.ACTION_CANCEL) {
        parent.post(new Runnable() { public void run() {
            final LinearLayout popup = (LinearLayout) parent.getRootView().findViewById(R.id.popup);
            if (popup != null) { // hide popup
              popup.removeAllViews();
              popup.setBackgroundColor(0x00000000);
              parent.lastMatch="";
              parent.currentButton = null;
            }
            if (finger != null) {
              finger.setVisibility(View.INVISIBLE);
            }
          } });
        return false;
      } else {
        SplashLayout sp = (SplashLayout) parent.getRootView().findViewById(R.id.SplashLayout);
        int pointerCount = ev.getPointerCount();
        int[] wvLocation = new int[2];
        parent.webView.getLocationOnScreen(wvLocation);
        int[] butLocation = new int[2];
        v.getLocationOnScreen(butLocation);
        for (int p = 0; p < pointerCount; p++) {
          float rawx = ev.getX(p) + butLocation[0];
          float rawy = ev.getY(p) + butLocation[1] - 64.0f * scale;
          if (rawx >= wvLocation[0] && rawx < wvLocation[0] + parent.webView.getWidth()) {
            if (rawy >= wvLocation[1] && rawy < wvLocation[1] + parent.webView.getHeight()) {
              long epoch = System.currentTimeMillis();
              if (epoch - lastCall > 250 ||
              ((lastCallX - rawx) * (lastCallX - rawx) + 
              (lastCallY - rawy) * (lastCallY - rawy)) > 5) {
                lastCall = epoch;
                lastCallX = rawx;
                lastCallY = rawy;
                //if (sp != null) sp.addSparkle(rawx, rawy,1);
                final int frawx = (int)rawx;
                final int frawy = (int) rawy;
                if (finger==null) {
                  finger = (ImageView) getRootView().findViewById(R.id.finger);
                  if (finger != null) {
                    finger.setAlpha(128);
                  }
                }
                if (finger != null) parent.post(new Runnable() { public void run() {
                    finger.setVisibility(View.VISIBLE);
                    finger.setLayoutParams(new AbsoluteLayout.LayoutParams(
                        finger.getWidth(), finger.getHeight(),
                        frawx - finger.getWidth()/2, frawy+10));
                        }});
                float webkitScale = parent.webView.getScale();
                float x = (rawx - wvLocation[0]) / webkitScale;
                float y = (rawy - wvLocation[1]) / webkitScale;
                String url = "javascript:window.RIKAI_getTextAtCarrot(" + x + "," + y + 
                "," + rawx + "," + rawy + ");";
                parent.currentButton = mybut;
                //parent.webView.getSettings().setJavaScriptEnabled(true);
                parent.webView.loadUrl(url);
              }
            }
          }
        }
        return true;
      }
    }
  }

  public class RikaiJavascriptHandler {
    private RikaiWebView parent;
    Integer handleCount;
    float scale;

    RikaiJavascriptHandler(RikaiWebView parent_) {
      parent = parent_;
      handleCount = new Integer(0);
      scale = parent.getContext().getResources().getDisplayMetrics().density;
    }

    @SuppressWarnings("unused")
    public void log(String text) {
      Log.e("RIKAI",text);
    }

    @SuppressWarnings("unused")
    public void registerTitle(String title, String url) {
      Log.e("RIKAI","registerTitle(" + title + "," + url + ")");
      if (title!=null && url != null && title.length()!=0 && url.length() != 0) {
        try {
          parent.urlToTitle.put(new URL(url).toExternalForm(),title);
        } catch (Throwable t) {
          log(t.toString());
        }
      }
    }

    @SuppressWarnings("unused")
    public void disableJavaScript() {
      //Log.e("RIKAI","Javascript disabled");
      //parent.webView.getSettings().setJavaScriptEnabled(false);
    }

    @SuppressWarnings("unused")
    public void toast(String text) {
 	    final String txtcp = text;
      parent.post(new Runnable() {
    	  public void run() {
          // this is called from JS with passed value
          Toast.makeText(parent.getContext(), txtcp, 1000).show();
    	  }
      });
    }
    @SuppressWarnings("unused")
    public void registerText(float x, float y, String oText) {
      int handleCountSave;
      synchronized(handleCount) {
        if (oText.equals(parent.lastMatch)) return;
        parent.lastMatch = oText;
        handleCount = handleCount + 1;
        handleCountSave = handleCount;
      }
      final float fx = x;
      final float fy = y;
      final String fOText = oText;
      final int fCount = handleCountSave;
      new Thread(
          new Runnable() {
            public void run() {
              registerTextBG(fx,fy,fOText,fCount);
            }
          }
        ).start();
    }
    private void matchAndAdd(byte[] pattern, LinkedHashMap<String,Vector<String> > done, LinkedHashMap<String,Vector<String> > found) {
      String key = "";
      try { key = new String(pattern,"EUC-JP"); } catch (UnsupportedEncodingException e) { }
      if (done.containsKey(key)) return;
      Vector<byte[]> finds = parent.edict.binSearch(pattern);
      Vector<String> ret = new Vector<String>();
      int matches=0;
      for (Iterator<byte[]> f = finds.iterator(); f.hasNext();) {
        byte[] bb = f.next();
        try { ret.addElement(new String(bb,"EUC-JP")); } catch (UnsupportedEncodingException e) { }
        matches += 1;
      }
      done.put(key,ret);
      if (matches>0) found.put(key,ret);
    }
    private void searchEdict(String key, LinkedHashMap<String,Vector<String> > done, LinkedHashMap<String,Vector<String> > found, boolean recurse, boolean allowSwitch) {
      if (done.containsKey(key)) return;
      if (found.size() > 3) return;
      if (key.length() == 0) return;
      if (recurse && allowSwitch) {
        int l = 0;
        while (l < key.length() && key.charAt(l) > 128) { l+=1; }
        if (l==0) return; // no japanese at start
        if (l < key.length()) {
          searchEdict(key.substring(0,l), done, found, recurse, allowSwitch);
          return;
        }
      }

      if (key.length() < 15) {
        try {
          // check a straight match
          byte[] pattern = (key + " ").getBytes("EUC-JP");
          matchAndAdd(pattern,done,found);
          // check for an '=' match if we start with hiragana
          if ((pattern[0] & 0xFF) == 0xa4) {
            pattern[pattern.length-1] = '=';
            Vector<byte[]> finds = parent.edict.binSearch(pattern);
            for (Iterator<byte[]> f = finds.iterator(); f.hasNext();) {
              byte[] bb = f.next();
              String[] parts = new String(bb,"EUC-JP").split("=");
              if (parts.length==2 && recurse) {
                String[] kanjiChoices = parts[1].split(",");
                for (int juku = 0; juku < kanjiChoices.length; juku++) {
                  searchEdict(kanjiChoices[juku] + " [" + parts[0] + "]", done, found, false, true); // recurse on right-hand-side
                }
              }
            }
            pattern[pattern.length-1] = ' ';
          }
          // attempt reverse conjugation
          for (Iterator<VConjData> it = vconj.iterator(); it.hasNext();) {
            VConjData vc = it.next();
            if (pattern.length - 1 < vc.derivedSuffix.length) continue;
            if (parent.edict.compare(pattern,pattern.length - vc.derivedSuffix.length - 1,
                  vc.derivedSuffix, 0, vc.derivedSuffix.length)==0) {
              byte[] newmsg = new byte[pattern.length - vc.derivedSuffix.length + vc.coreSuffix.length - 1];
              for (int j=0; j<pattern.length - 1 - vc.derivedSuffix.length; ++j) { newmsg[j] = pattern[j]; }
              for (int j=0; j < vc.coreSuffix.length; j+=1) {
                newmsg[pattern.length - 1 - vc.derivedSuffix.length + j] = vc.coreSuffix[j];
              }
              if (recurse) searchEdict(new String(newmsg,"EUC-JP"), done, found, false, true); // recurse
            }
          }
          // attempt smaller substring
          if (recurse) searchEdict(key.substring(0,key.length()-1),done, found, true, false);

          // attempt kana switch
          if (recurse && allowSwitch) {
            boolean didSwitch = false;
            byte[] switchPattern = new byte[pattern.length-1];
            for (int i=0; i<pattern.length-1; ++i) {
              int c1 = pattern[i] & 0xFF;
              if (c1>=0xa1 && i<pattern.length-2) { // first byte kanji
                if (c1==0xa4) {
                  switchPattern[i] = (byte)(0xa5 - 0x100); // switch hira -> kata
                  didSwitch=true;
                } else if (c1==0xa5) {
                  switchPattern[i] = (byte)(0xa4 - 0x100); // switch kata -> hira
                  didSwitch=true;
                } else {
                  switchPattern[i] = pattern[i];
                }
                ++i;
                int c2 = pattern[i] & 0xFF;
              }
              switchPattern[i] = pattern[i];
            }
            if (didSwitch) {
              //Log.e("RIKAI","Did switch: first byte==" + (switchPattern[0] & 0xFF));
              final String tt = new String(switchPattern,"EUC-JP");
              /*parent.post(new Runnable() { public void run() { Toast.makeText(parent.getContext(),
                    "<" + tt + ">", 1000).show(); } });*/
              searchEdict(tt, done, found, recurse, false);
            }
          }
        } catch (Throwable t) {
          Log.e("RIKAI",t.toString() + ":" + t.getMessage());
        }
      } else {
        if (recurse) searchEdict(key.substring(0,14),done, found, true, allowSwitch);
      }
    }
    private String escapeHTML(String s) {
      s = s.replaceAll("&","&amp;");
      s = s.replaceAll("<","&lt;");
      s = s.replaceAll(">","&gt;");
      return s;
    }
    private View createKanjidicDefinition(String msg) {
      if (msg.length()<=2) return new TextView(parent.getContext());
      String kanji = msg.substring(0,1);
      int pos = 2;
      boolean nanori = false;
      StringBuffer meanings = new StringBuffer(); 
      StringBuffer yomikata = new StringBuffer(); 
      while (pos < msg.length()) {
        int npos = pos;
        char c = msg.charAt(pos);
        if (c == '{') { // skip to closing brace
          while (npos < msg.length() && msg.charAt(npos) != '}') npos++;
        }
        while (npos < msg.length() && msg.charAt(npos) != ' ') npos++;
        if (npos < msg.length() && msg.charAt(npos) == ' ') npos++;

        switch (c) {
          case 'T': // switch to nanori
            nanori = true;
            break;
          case '{': // english
            if (npos - pos > 2) {
              meanings.append(msg.substring(pos+1,npos-2));
              meanings.append("; ");
            }
            break;
          case 'B': // bushu
          case 'C': // hist bushu
          case 'F': // freq of use
          case 'G': // grade
          case 'H': // Halpern
          case 'N': // Nelson
          case 'P': // skip
          case 'S': // stroke count
          case 'U': // unicode
          case 'I': // Spahn & Hadamitzky 
          case 'Q': // four corner code
          case 'M': // MN/MP == Morohashi index
          case 'E': // Henshall index
          case 'K': // Gakken index
          case 'L': // Heisig index
          case 'O': // Japanese names index
          case 'W': // korean reading
          case 'Y': // pinyin reading
          case 'X': // cross ref to nelson
          case 'Z': // misclassified code
            // unhandled
            break;
          default:
            if ((c >= '\u3030' && c <= '\u30ff') || c == '-') { // kana range, or prefix
              String[] splits = (msg.substring(pos,npos-1)).split("\\.");
              yomikata.append("<font color=\"#CCFFCC\">");
              yomikata.append(escapeHTML(splits[0]));
              yomikata.append("</font>");
              if (splits.length > 1) {
                yomikata.append("<font color=\"#FFCCCC\">");
                yomikata.append(escapeHTML(splits[1]));
                yomikata.append("</font>");
              }
              yomikata.append(" ; ");
              /*if (c >= '\u3040' && c <= '\u309f') { // hiragana
                kunyomi.append("<font color=\"#splits[0]
                } else if (c>='\u30a0' && c <= '\u30ff') { // katakana
                }*/
            }
        }
        pos = npos;
      }

      LinearLayout ll = new LinearLayout(parent.getContext());
      ll.setOrientation(LinearLayout.VERTICAL);
      {
        LinearLayout ll2 = new LinearLayout(parent.getContext());
        ll2.setOrientation(LinearLayout.HORIZONTAL);
        {
          TextView tv = new TextView(parent.getContext());
          tv.setTextColor(0xFFFFFFFF);
          tv.setShadowLayer(2.0f,1.0f,1.0f,0xFF000000);
          tv.setText(Html.fromHtml("<font color=\"#CCFFFF\">" + escapeHTML(kanji) + "</font>"));
          tv.setTextSize(20 * scale + 0.5f);
          ll2.addView(tv);
        }
        ll.addView(ll2);
      }
      { // yomikata
        TextView tv = new TextView(parent.getContext());
        tv.setText(Html.fromHtml(yomikata.toString()));
        tv.setTextSize(12 * scale + 0.5f);
        ll.addView(tv);
      }
      { // defs
        TextView tv = new TextView(parent.getContext());
        tv.setText(Html.fromHtml("<font color=\"#FFFFFF\">" + escapeHTML(meanings.toString()) + "</font>"));
        tv.setTextSize(12 * scale + 0.5f);
        ll.addView(tv);
      }
      return ll;
    }
    private View createDictionaryDefinition(String msg, boolean landscape) {
      String kanji = "";
      String yomi = "";
      String definitions = "";
      int firstSpace = msg.indexOf(' ');
      if (firstSpace>0) {
        kanji = escapeHTML(msg.substring(0,firstSpace));
        int openBracket = msg.indexOf('[',firstSpace);
        if (openBracket>0) {
          int closeBracket = msg.indexOf(']', openBracket);
          if (closeBracket>0) {
            yomi = escapeHTML(msg.substring(openBracket + 1, closeBracket));
          }
        }
        int openSlash = msg.indexOf('/', firstSpace);
        int lastSlash = msg.lastIndexOf('/');
        if (openSlash>0 && lastSlash>0) {
          definitions = escapeHTML(msg.substring(openSlash+1, lastSlash));
        }
      }
      LinearLayout ll = new LinearLayout(parent.getContext());
      ll.setOrientation(LinearLayout.VERTICAL);
      {
        LinearLayout ll2 = new LinearLayout(parent.getContext());
        if (landscape) {
          ll2.setOrientation(LinearLayout.HORIZONTAL);
        } else {
          ll2.setOrientation(LinearLayout.VERTICAL);
        }

        {
          TextView tv = new TextView(parent.getContext());
          tv.setTextColor(0xFFFFFFFF);
          tv.setShadowLayer(2.0f,1.0f,1.0f,0xFF000000);
          tv.setText(Html.fromHtml("<b><font color=\"#CCFFFF\">" + kanji + "  </font></b>"));
          tv.setTextSize(14 * scale + 0.5f);
          ll2.addView(tv);
        }
        {
          TextView tv = new TextView(parent.getContext());
          tv.setTextColor(0xFFFFFFFF);
          tv.setShadowLayer(2.0f,1.0f,1.0f,0xFF000000);
          tv.setText(Html.fromHtml("<font color=\"#EEFFEE\">" + yomi + "</font>"));
          tv.setTextSize(10 * scale + 0.5f);
          ll2.addView(tv);
        }

        ll.addView(ll2);
      }
      {
        TextView tv = new TextView(parent.getContext());
        tv.setTextColor(0xFFFFFFFF);
        tv.setShadowLayer(2.0f,1.0f,1.0f,0xFF000000);
        tv.setText(Html.fromHtml(definitions));
        tv.setTextSize(10 * scale + 0.5f);
        ll.addView(tv);
      }
      
      return ll;
    }
    private synchronized void registerTextBG(float x, float y, String oText, int count) {
      synchronized(handleCount) {
        if (oText != parent.lastMatch || 
            count != handleCount) return;
      }
      oText = oText.trim();
      Vector<String> finds = new Vector<String>();
      try { 
        int bgColor=0;
        final LinearLayout popup = (LinearLayout) parent.getRootView().findViewById(R.id.popup);
        if (parent.currentButton == parent.kanjiButton) {
          bgColor=0xCC666633;
        } else if (parent.currentButton == parent.dictButton) {
          bgColor=0xCC333399;
        } else if (parent.currentButton == null) {
          parent.post(new Runnable() { public void run() {
            popup.removeAllViews();
            popup.setBackgroundColor(0x00000000);
            } });
          return;
        }
        final int fBGColor = bgColor;
        SplashLayout sp = (SplashLayout) parent.getRootView().findViewById(R.id.SplashLayout);
        int[] spLocation = new int[2];
        sp.getLocationOnScreen(spLocation);
        int[] popupLocation = new int[2];
        popup.getLocationOnScreen(popupLocation);
        /*Log.e("RIKAI","sp(" + spLocation[0] + "," + spLocation[1] + "," + sp.getWidth() + "," + sp.getHeight() +
        "), pop(" + popupLocation[0] + "," + popupLocation[1] + "," + popup.getWidth() + "," + popup.getHeight() + ")");*/
        int popupx=0; int popupy=0; int popupw = sp.getWidth(); int popuph = ViewGroup.LayoutParams.WRAP_CONTENT;
        if (x>spLocation[0] + sp.getWidth()/2) {
          popupw /= 2;
        } else {
          popupw /= 2;
          popupx += popupw;
        }
        final int fpopupx = popupx;
        final int fpopupy = popupy;
        final int fpopupw = popupw;
        final int fpopuph = popuph;
        /*Log.e("RIKAI","after sp(" + spLocation[0] + "," + spLocation[1] + "," + sp.getWidth() + "," + sp.getHeight() +
        "), pop(" + popupLocation[0] + "," + popupLocation[1] + "," + popup.getWidth() + "," + popup.getHeight() + ")");*/

        if (parent.currentButton == parent.dictButton) {
          LinkedHashMap<String,Vector<String> > searched = new LinkedHashMap<String,Vector<String> >();
          LinkedHashMap<String,Vector<String> > found = new LinkedHashMap<String,Vector<String> >();
          searchEdict(oText.trim(), searched, found, true, true);
          // now deal with actual matches
          final Vector<View> tvs = new Vector<View>();
          //Log.e("RIKAI","Got " + found.size() + " matches");
          for (Map.Entry<String, Vector<String> > vv: found.entrySet()) {
            Vector<String> v = vv.getValue();
            if (v.size() > 0) {
              for (Iterator<String> ff = v.iterator(); ff.hasNext(); ) {
                tvs.addElement(createDictionaryDefinition(ff.next(), sp.getWidth() > sp.getHeight()));
              }
            }
          }
          parent.post(new Runnable() { public void run() {
            popup.removeAllViews();
            popup.setBackgroundColor(fBGColor);
            for (Iterator<View> tvi = tvs.iterator(); tvi.hasNext(); ) {
              popup.addView(tvi.next());
            }
            popup.setLayoutParams(new AbsoluteLayout.LayoutParams(
              fpopupw, fpopuph, fpopupx, fpopupy));
          }});
        } // edict button
        else if (parent.currentButton == parent.kanjiButton) {
          byte[] pattern = (oText.trim().substring(0,1) + " ").getBytes("EUC-JP");
          Vector<byte[]> found = parent.kanjidic.binSearch(pattern);
          final Vector<View> tvs = new Vector<View>();
          for (Iterator<byte[]> f = found.iterator(); f.hasNext(); ) {
            byte[] v = f.next();
            if (v.length > 0) {
              tvs.addElement(createKanjidicDefinition(new String(v,"EUC-JP")));
            }
          }
          parent.post(new Runnable() { public void run() {
              popup.removeAllViews();
              popup.setBackgroundColor(fBGColor);
              for (Iterator<View> tvi = tvs.iterator(); tvi.hasNext(); ) {
                popup.addView(tvi.next());
              }
              popup.setLayoutParams(new AbsoluteLayout.LayoutParams(
                  fpopupw, fpopuph, fpopupx, fpopupy));
              }});
        }
      } catch (Throwable e) {
        Log.e("RIKAI",e + ":" + e.getMessage());
      }
    }
  }

  // WebChromeClient
  private class RikaiChrome extends WebChromeClient {
    private RikaiWebView parent;
    public RikaiChrome(RikaiWebView parent_) {
      parent = parent_;
    }
    @Override
    public void onConsoleMessage(String message, int lineNumber, String sourceID) {
      Log.e("RIKAI", sourceID+":"+lineNumber+":"+message);
    }
    /*@Override
    public boolean onConsoleMessage(ConsoleMessage cm) {
      Log.e("RIKAI", cm.sourceId()+":"+cm.lineNumber()+":"+cm.message());
      return true;
    }*/
    @Override
    public boolean onJsAlert(WebView v, String url, String message, JsResult result) {
    	Log.e("RIKAI",message);
    	//return super.onJsAlert(v,url,message,result);
    	return true;
    }
    public void onProgressChanged(WebView view, int progress) {
      final ProgressBar pb = parent.progress;
      final int fProgress = progress;
      parent.post(new Runnable() { public void run() {
          if (pb != null) {
            pb.setProgress(fProgress);
            pb.invalidate();
          }
          }});
    }
  }
  
  // WebViewClient
  private class RikaiClient extends WebViewClient {
    private boolean recursing;
    private RikaiWebView parent;
    private String insertion_code;
    private String insertion_js;
    private String insertion_css;
    private String insertion_css_js;
    private Thread t;
    public RikaiClient(RikaiWebView parent_) {
      parent = parent_;
      try {
        InputStream ins = getResources().openRawResource(R.raw.insertion_code);
        int size = ins.available();
        byte[] buffer = new byte[size];
        ins.read(buffer);
        ins.close();
        insertion_code = new String(buffer);
        insertion_js = "javascript:" + (insertion_code.replace('\n',' ').trim());
      } catch (Throwable e) {
        Toast.makeText(getContext(), "IOException " + e.toString(), 1000).show();
      }
      try { // load css
        InputStream ins = getResources().openRawResource(R.raw.ruby);
        int size = ins.available();
        byte[] buffer = new byte[size];
        ins.read(buffer);
        ins.close();
        insertion_css = new String(buffer,"ISO-8859-1");
        insertion_css_js =  ("javascript:window.RIKAI_insert_css_once(\"ruby\", \"" + insertion_css.replace("\r\n","\\n").replace("\n","\\n").replace(">","&gt;") + "\");").trim();
      } catch (Throwable e) {
        Toast.makeText(getContext(), "IOException " + e.toString(), 1000).show();
      }
    }
    private void loadCode() {
      parent.webView.loadUrl(insertion_js);
      //Log.e("RIKAI",insertion_css_js);
      parent.webView.loadUrl(insertion_css_js);
    }
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      String home = parent.homepage;
      parent.setLastURL(url);
      parent.webView.getSettings().setBlockNetworkImage(false);
      view.loadUrl(url);
      return true;
    }
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
      parent.post(new Runnable() { public void run() {
          if (parent.progress != null) {
            parent.removeView(parent.progress);
          }
          parent.progress = new ProgressBar(parent.getContext(),null, android.R.attr.progressBarStyleHorizontal);
          parent.progress.setMax(100);
          parent.progress.setIndeterminate(false);
          parent.progress.setProgress(0);
          parent.addView(progress,0,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0.01F));
          }});
    }


        // Read the entire resource into a local byte buffer.
    public void onPageFinished(WebView view, String url) { 
      String home = parent.homepage;
      if (url.equals(home) && !recursing) {
        recursing=true;
        parent.setLastURL(url);
        view.loadDataWithBaseURL(home, parent.bookmarks.data, 
            parent.bookmarks.attributes.get("content-type"), "utf-8", 
            home);
      } else {
        recursing=false;
      }

      loadCode();
      parent.post(new Runnable() { public void run() {
        if (parent.progress != null) {
            parent.removeView(parent.progress);
          }
        }});
      parent.webView.loadUrl("javascript:window.RIKAI_replaceAds();"); 
    }

    public void onLoadResource(WebView view, String url) {
      webView.getSettings().setBlockNetworkImage(true);
      String s = url.toLowerCase();
      if (s.endsWith(".jpg") || s.endsWith(".jpeg") || s.endsWith(".gif") || s.endsWith(".png") || s.endsWith(".css")) {
        loadCode();
      }
    }
    
  }
  // LongClickListener
  private class RikaiLongClickHandler implements OnLongClickListener {
    private RikaiWebView parent;
    public RikaiLongClickHandler(RikaiWebView p) {
      parent = p;
    }
    public boolean onLongClick(View v) {
      HitTestResult result = parent.webView.getHitTestResult();
      if (result.getType() == HitTestResult.SRC_ANCHOR_TYPE ||
        result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
        final CharSequence[] items = {"Remember Link", /*"Save Page Offline",*/ "Remove Link"};
        final String extra = result.getExtra();
        //Log.e("RIKAI", "Long click on " + extra);
        AlertDialog.Builder builder = new AlertDialog.Builder(parent.context);
        builder.setTitle("Link Options:");
        builder.setItems(items, new DialogInterface.OnClickListener() {
          public void onClick(DialogInterface dialog, int item) {
            if (items[item].equals("Remember Link")) {
              parent.rememberLink(extra);
            } else if (items[item].equals("Save Page Offline")) {
              parent.saveLink(extra);
            } else if (items[item].equals("Remove Link")) {
              parent.removeLink(extra);
            } else {
              Toast.makeText(parent.context, "Unhandled: " + items[item] + " '" + extra + "'", Toast.LENGTH_SHORT).show();
            }
          }
        });
        AlertDialog alert = builder.create();
        alert.show();
      } else {
        Log.e("RIKAI", "Long click with unhandled type " + result.getType());
      }
      return true;
    }
  }

  private void setEdict(String name) {
     try { 
       if (edict == null) edict = data.getCompressedReader(name);
     } catch (IOException e) {
       Log.e("RIKAI",e.getMessage());
     }
  }
  private void setKanjidic(String name) {
     try { 
       if (kanjidic == null) kanjidic = data.getCompressedReader(name);
     } catch (IOException e) {
       Log.e("RIKAI",e.getMessage());
     }
  }

  public class ParsedFile implements Comparable {
    public Map<String, String> attributes;
    String data;
    public ParsedFile() {
      attributes = new HashMap<String, String>();
      attributes.put("title","");
      data="";
    }
    public ParsedFile(String txt) {
      fromString(txt);
    }
    public void fromString(String txt) {
      String[] lines = txt.split("\n");
      attributes = new HashMap<String, String>();
      attributes.put("title","");
      StringBuffer buf = new StringBuffer();
      int i;
      for (i=0; i<lines.length; i++) {
        if (lines[i].equals("")) break;
        int ii = lines[i].indexOf(':');
        if (ii!=-1) {
          attributes.put(
              lines[i].substring(0,ii).trim().toLowerCase(),
              lines[i].substring(ii+1).trim());
        }
      }
      for (i+=1 ;i<lines.length; i++) {
        buf.append(lines[i]);
        buf.append('\n');
      }
      data = buf.toString();
    }
    public ParsedFile(File f) throws IOException, FileNotFoundException {
      BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f)));
      StringBuffer sb = new StringBuffer();
      String line;
      while ( (line = r.readLine()) != null) {
        sb.append(line); sb.append("\n");
      }
      fromString(sb.toString());
    }
    public String toString() {
      StringBuffer buf = new StringBuffer();
      if (attributes != null) {
        for (Map.Entry<String,String> entry : attributes.entrySet()) {
          buf.append(entry.getKey());
          buf.append(": ");
          buf.append(entry.getValue());
          buf.append("\n");
        }
      }
      buf.append("\n");
      buf.append(data);
      return buf.toString();
    }
    public int compareTo(Object o) { // to be compared, elements must have a 'title' attribute
      ParsedFile oPf = (ParsedFile)o;
      String t1 = attributes.get("title");
      String t2 = oPf.attributes.get("title");
      if (t1 == null) t1 = "";
      if (t2 == null) t2 = "";
      int c = t1.compareTo(t2);
      if (c!=0) return c;
      else return data.compareTo(oPf.data);
    }
    @Override
      public int hashCode() {
        String type = attributes.get("content-type");
        if (type == null) return 0;
        String url;
        if (type.equals("text/url")) {
          url = data;
        } else {
          url = attributes.get("from");
        }
        if (url == null) return 0;
        return url.hashCode();
      }
      public String getFrom() { // gets url this came from, whether text/url or something else
        if (attributes.containsKey("from")) return attributes.get("from").trim();
        else if (attributes.get("content-type").equals("text/url")) {
          return data.trim();
        } else return "";
      }
  }

  public void rememberLink(String url) {
    if (url == null) return;
    final ParsedFile pf = new ParsedFile();
    pf.attributes.put("content-type","text/url");
    pf.attributes.put("title","");
    pf.data = url;
    final String fn = bookmarksDir + "/" + getBookmarkFilename(bookmarksDir, pf, null);
    AlertDialog.Builder alert = new AlertDialog.Builder(context);
    alert.setTitle("Save bookmark");
    alert.setMessage("Please enter a title for this bookmark");
    final EditText input = new EditText(context);
    String cachedTitle = urlToTitle.get(url);
    if (cachedTitle != null && cachedTitle.length()>0) {
      input.setText(cachedTitle);
    }
    alert.setView(input);
    final RikaiWebView rkv = this;
    alert.setPositiveButton("Save", new DialogInterface.OnClickListener() {
      public void onClick(DialogInterface dialog, int whichButton) {
        try {
          String title = input.getText().toString().trim();
          if (title.equals("")) return;
          title = Html.toHtml(new SpannableString(input.getText().toString())).trim();
          if (title.equals("")) return;
          pf.attributes.put("title", title.replaceAll("\n"," "));
          BufferedWriter bw = new BufferedWriter(new FileWriter(fn));
          bw.write(pf.toString());
          bw.close();
          rkv.bookmarks = generateBookmarks();
          if (rkv.getLastURL(homepage).equals(homepage)) {
            rkv.loadUrl(homepage);
          }
        } catch (Throwable t) {
          Log.e("RIKAI",t.toString());
        }
      }
    });
    alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int whichButton) {
        // canceled.
        }});
    alert.show();
  }
  private void saveLink(String url) {

    bookmarks = generateBookmarks();
    if (getLastURL(homepage).equals(homepage)) {
      loadUrl(homepage);
    }
  }
  private void removeLink(String url) {
    ParsedFile pf = new ParsedFile();
    pf.attributes.put("content-type","text/url");
    pf.attributes.put("title","");
    pf.data = url;
    String fn = getBookmarkFilename(bookmarksDir, pf, null);
    new File(bookmarksDir + "/" + fn).delete();
    bookmarks = generateBookmarks();
    if (getLastURL(homepage).equals(homepage)) {
      loadUrl(homepage);
    }
  }

  private String getBookmarkFilename(File dir, ParsedFile pf, File[] filelist) {
    // You may call this with filelist == null and it'll generate the filelist 
    // each time.
    if (filelist == null) filelist = dir.listFiles();
    int hc = pf.hashCode();
    String prefix = String.format("%05X_", hc & (256*256*16-1));
    Set<String> there = new HashSet<String>();
    String from = pf.getFrom();
    for (int i=0; i< filelist.length; ++i) {
      String name = filelist[i].toString();
      if (name.startsWith(prefix)) {
        try {
          ParsedFile diskpf = new ParsedFile(filelist[i]);
          //Log.e("RIKAI", "Comparing " + filelist[i].toString() + " for " + diskpf.getFrom());
          if (diskpf.getFrom().equals(from)) return name;
        } catch (Throwable t) {
          Log.e("RIKAI", "while reading pf: " + name);
        }
        there.add(name);
      }
    }
    // if we got here, we need to find a free slot
    int suffix = 0;
    String fn;
    do {
      suffix += 1;
      fn = prefix + String.format("%02x.bmk", suffix);
    } while (there.contains(fn));
    //Log.e("RIKAI","getBookmarkFilename returning " + fn);
    return fn;
  }
    
  private ParsedFile generateBookmarks() {
    ParsedFile ret = new ParsedFile();
    ret.attributes.put("content-type", "text/html");
    ret.attributes.put("title","home");
    ret.attributes.put("from",homepage);
    StringBuffer retsb = new StringBuffer();
    retsb.append("<html><head>\n");
    retsb.append("</head><body>");
    File dir = bookmarksDir;
    File[] files = dir.listFiles();
    TreeSet<ParsedFile> urls = new TreeSet<ParsedFile>();
    TreeSet<ParsedFile> offline = new TreeSet<ParsedFile>();
    try {
      if (files.length == 0) { // generate default bookmarks if there are none
        BufferedReader r = new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.standard_urls)));
        String line;
        while ( (line = r.readLine()) != null) {
          String[] cols = line.split("[\t\n]");
          if (cols.length >= 2) {
            String url = cols[0];
            String title = cols[1];
            //Log.e("RIKAI","TITLE = " + title);
            ParsedFile pf = new ParsedFile();
            pf.attributes.put("content-type","text/url");
            pf.attributes.put("title",title);
            pf.data = new URL(url).toExternalForm();
            BufferedWriter bw = new BufferedWriter(new FileWriter(dir.toString() + "/" + getBookmarkFilename(dir, pf, null)));
            bw.write(pf.toString());
            bw.close();
          }
        }
      }
      files = dir.listFiles();
    } catch (Throwable t) {
      Log.e("RIKAI","While reading default list " + t.toString() + ":" + t.getMessage());
    }
    for (int i=0; i<files.length; i++) {
      try {
        ParsedFile pf = new ParsedFile(files[i]);
        String type = pf.attributes.get("content-type");
        if (type != null) {
          if (type.equals("text/url")) {
            urls.add(pf);
          } else {
            offline.add(pf);
          }
        }
      } catch (Throwable t) {
        Log.e("RIKAI","Cannot read " + files[i].toString()+ ":" + t.toString());
      }
    }
    retsb.append("<h2>Bookmarked Links:</h2>");
    retsb.append("<table class='urlList'>");
    for (ParsedFile pf : urls) {
      StringBuffer rowsb = new StringBuffer();
      try {
        rowsb.append("<tr><td><a href='");
        //rowsb.append(Html.toHtml(new SpannableString(pf.data.trim()))); // FIXME: encode entities. But this attempt doesn't work
        rowsb.append(pf.data.trim());
        rowsb.append("'>");
        rowsb.append(pf.attributes.get("title"));
        rowsb.append("</td></tr>\n");
      } catch (Throwable t) { 
        Log.e("RIKAI","in url bookmark generation: " + t.toString());
      }
      retsb.append(rowsb.toString());
    }
    retsb.append("</table>");
    //retsb.append("<h2>Locally downloaded</h2>"); // TODO: implement
    ret.data = retsb.toString();
    return ret;
  }
  
  private void copyFiles() {
    String dir = Environment.getExternalStorageDirectory() + "/" + getResources().getString(R.string.app_name);
    if (!new File(dir).getParentFile().canWrite()) {
       post(new Runnable() { public void run() { Toast.makeText(getContext(), "SD card not found", 1000).show(); } });
       return;
    }
    final File edictFile = new File(dir + "/rk_edict.dat");
    if (!edictFile.canRead() || edictFile.length()==0) {
      Log.e("RIKAI","Didn't find dictionary at " + edictFile.getPath());
      edictFile.getParentFile().mkdirs();
      final File edictFileTmp = new File(dir + "/rk_edict.tmp");
      edictFileTmp.delete();
      final RikaiWebView self = this;
      //post(new Runnable() { public void run() { Toast.makeText(getContext(), "Copying edict to local filesystem (one time only)", 500).show(); } });
      final ProgressDialog dialog = ProgressDialog.show(
          getContext(), "",
          "Pulling edict from the internet to local storage (one time only). Please wait...", true);
      new Thread(new Runnable() { public void run() { try {
          URL url = new URL("http://www.rikai.com/android/edict_000.dat.gz");
          Data.compressStream(new GZIPInputStream(new BufferedInputStream(url.openStream())),
            edictFileTmp.getPath());
          edictFileTmp.renameTo(edictFile);
          self.post(new Runnable() { public void run() {
            self.setEdict(edictFile.getPath());
            Toast.makeText(getContext(), "Finished copying edict (one time only)", 500).show(); } });
            dialog.dismiss();
          } catch (Throwable t) { Log.e("RIKAI",t.toString());}
          }}).start();
    } else {
      Log.e("RIKAI", "Found dictionary, size == " + edictFile.length());
      setEdict(edictFile.getPath());
    }
    final File kdicFile = new File(dir + "/rk_kdic.dat");
    if (!kdicFile.canRead() || kdicFile.length()==0) {
      kdicFile.getParentFile().mkdirs();
      final File kdicFileTmp = new File(dir + "/rk_kdic.tmp");
      kdicFileTmp.delete();
      final RikaiWebView self = this;
      final ProgressDialog dialog = ProgressDialog.show(
          getContext(), "",
          "Pulling kanjidic from the internet to local storage (one time only). Please wait...", true);
      new Thread(new Runnable() { public void run() { try {
          URL url = new URL("http://www.rikai.com/android/kanjidic.dat.gz");
          Data.compressStream(new GZIPInputStream(new BufferedInputStream(url.openStream())),
            kdicFileTmp.getPath());

          kdicFileTmp.renameTo(kdicFile);
          self.post(new Runnable() { public void run() {
            self.setKanjidic(kdicFile.getPath());
            Toast.makeText(getContext(), "Finished copying kanjidic (one time only)", 500).show(); } });
            dialog.dismiss();
          } catch (Throwable t) { Log.e("RIKAI",t.toString());}
          }}).start();
    } else {
      Log.e("RIKAI", "Found dictionary, size == " + kdicFile.length());
      setKanjidic(kdicFile.getPath());
    }
    bookmarksDir = new File(dir + "/bookmarks");
    if (!bookmarksDir.canRead()) {
      bookmarksDir.mkdirs();
    }
    bookmarks = generateBookmarks();
  }
  private void loadVConj() {
    try {
      vconj = new Vector<VConjData>();
      HashMap<Integer,String> tagMap = new HashMap<Integer,String>();
      BufferedReader in = new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.vconj),"EUC-JP"));
      String line;
      while ((line = in.readLine()) != null) {
        line = line.trim();
        if (line.length()>0) {
          if (line.charAt(0)=='#') continue;
          if (line.charAt(0)=='$') break;
          String[] cols = line.split("\t");
          if (cols.length == 2) {
            tagMap.put(Integer.parseInt(cols[0]),cols[1]);
          } else {
            Log.e("RIKAI", "Bad vconj tag line " + line);
          }
        }
      }
      while ((line = in.readLine()) != null) {
        line = line.trim();
        if (line.length()>0) {
          if (line.charAt(0) == '#') continue;
          String[] cols = line.split("\t");
          if (cols.length == 3) {
            VConjData vj = new VConjData();
            vj.derivedSuffix = cols[0].getBytes("EUC-JP");
            vj.coreSuffix = cols[1].getBytes("EUC-JP");
            vj.tag = tagMap.get(Integer.parseInt(cols[2]));
            vconj.addElement(vj);
          } else {
            Log.e("RIKAI","Bad vconj rule line " + line);
          }
        } 
      }
    } catch (Throwable t) {
      Log.e("RIKAI",t.toString() + ":" + t.getMessage());
      Toast.makeText(getContext(), t.toString() + ":" + t.getMessage(), 1000).show();
    }
  }

  // Private methods
  private void setup(Context c) {
    urlToTitle = new LinkedHashMap<String,String>(1, .75F, true) { // lru cache of last 100
      @Override
        protected boolean removeEldestEntry(Map.Entry<String,String> eldest) { return size() > 100; }
    };
    context = c;
    sp = c.getSharedPreferences("RIKAI", 0);
    try { homepage = new URL("http:///127.0.0.1/index.html").toExternalForm(); } catch (Throwable t) { Log.e("RIKAI",t.toString()); }
    loadVConj();
    data = new Data();
    copyFiles();
    setOrientation(LinearLayout.VERTICAL);


    webView = new WebView(c);
    webView.setLongClickable(true);
    webView.setOnLongClickListener(new RikaiLongClickHandler(this));
    webView.getSettings().setJavaScriptEnabled(true);
    webView.getSettings().setBlockNetworkImage(true);
    webView.getSettings().setLoadsImagesAutomatically(false);
    webView.addJavascriptInterface(new RikaiJavascriptHandler(this), "RIKAI");
    webView.setWebChromeClient(new RikaiChrome(this));
    webView.setWebViewClient(new RikaiClient(this));
    webView.getSettings().setBuiltInZoomControls(true);
    webView.setLayoutParams(new LinearLayout.LayoutParams(
      ViewGroup.LayoutParams.FILL_PARENT,
      ViewGroup.LayoutParams.FILL_PARENT, (float)1.0));
    addView(webView);

    LinearLayout buttons = new LinearLayout(c);
    buttons.setOrientation(LinearLayout.HORIZONTAL);
    buttons.setLayoutParams(new LinearLayout.LayoutParams(
      ViewGroup.LayoutParams.FILL_PARENT,
      ViewGroup.LayoutParams.WRAP_CONTENT, (float)0.01));

    dictButton = new Button(c);
    dictButton.setText("Word");
    dictButton.setLayoutParams(new LinearLayout.LayoutParams(
      ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT,
      (float)1.0));
    ((LinearLayout.LayoutParams)dictButton.getLayoutParams()).gravity =
    Gravity.CENTER_HORIZONTAL;  
    buttons.addView(dictButton);
    dictButton.setOnTouchListener(new TouchieTouchie(this,dictButton));

    kanjiButton = new Button(c);
    kanjiButton.setText("Kanji");
    kanjiButton.setLayoutParams(new LinearLayout.LayoutParams(
      ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT,
      (float)1.0));
    ((LinearLayout.LayoutParams)kanjiButton.getLayoutParams()).gravity =
    Gravity.CENTER_HORIZONTAL;  
    buttons.addView(kanjiButton);
    kanjiButton.setOnTouchListener(new TouchieTouchie(this,kanjiButton));

    addView(buttons);
    
  }
}
